openbao 0.7.0

Secure, typed, async Rust SDK for OpenBao
Documentation

OpenBao Rust SDK

openbao is a secure, typed, async Rust SDK for OpenBao, the community-driven open source fork of Vault. It is designed for audited secret workflows: HTTPS by default, no redirect forwarding, strict path validation, secret-aware token types, and a small reviewed dependency surface.

The crate name on crates.io is openbao; Rust imports are lowercase:

use openbao::Client;

This README documents the 0.7.0 development line. 0.7.0 builds on 0.6.0 with AppRole administration and the remaining identity/plugin-style engine coverage planned for the release.

The crate is dual-licensed under MIT or Apache-2.0.

Current Status

Implemented now:

  • Async client with typestate authentication.
  • Direct token authentication with re-exported openbao::SecretString.
  • AppRole login plus role and SecretID administration, with role IDs, SecretIDs, accessors, and returned tokens treated as secret material.
  • Kubernetes auth login plus config and role administration helpers.
  • TLS certificate auth login, method config, CA role, and CRL administration helpers.
  • JWT login plus JWT/OIDC auth method config and role administration helpers.
  • Userpass login plus user create/read/list/delete, password update, and policy update helpers.
  • Token create, lookup, accessor lookup/list, renew, revoke, and revoke-self helpers.
  • KV v2 read, write, CAS write, patch, list, latest delete, version read, version delete, undelete, destroy, metadata, backend config, typed data, and secret-aware service config read/write helpers.
  • KV v1 read, write, delete, and list helpers.
  • Cubbyhole read, optional read, write, delete, and list helpers for token-scoped handoff data.
  • Kubernetes secrets engine config, role create/read/list/delete, and service account credential generation helpers.
  • RabbitMQ secrets engine connection config, lease config, role create/read/list/delete, and dynamic credential helpers.
  • Database connection config, dynamic roles, static roles, root/static rotation, and credential helpers.
  • Identity entity, group, entity-alias, and group-alias lifecycle helpers.
  • LDAP secrets engine config, static role, dynamic role, credential, library checkout, and check-in helpers.
  • SSH role, zero-address role, IP lookup, OTP credential, issuer config, issuer list/submit/read/update/delete, CA public-key metadata, CA sign, generated certificate/key issue, and OTP verification helpers.
  • TOTP key create/read/list/delete, code generation, and code validation helpers.
  • PKI URL and CRL config, root/intermediate generation, intermediate signing and install, role write/read/list/delete, issue, sign, revoke, certificate list/read, issuer/key list/read/delete/update, issuer revoke, CA/key import, ACME config/EAB/directory URL, CRL rotate, and tidy helpers.
  • Transit key create, read, list, delete, encrypt, decrypt, rewrap, data key, random, hash, HMAC, sign, verify, typed RSA/JWS signing options, and optional raw-byte helpers.
  • System health, seal status, and loopback-only dev bootstrap helpers.
  • Secret and auth mount enable, list, read, tune, and disable helpers.
  • Response wrapping lookup, wrap, unwrap, and rewrap helpers.
  • ACL policy list, read, write, delete, and prefix list helpers.
  • Bounded ACL policy builder helpers for common KV v2 and Transit least-privilege rules.
  • Idempotent admin bootstrap plan builder for KV v2 mounts, Transit mounts, Transit keys, ACL policies, KV v2 string secret values, auth methods, AppRole roles, explicit scoped service-token issuance, and explicit AppRole SecretID issuance.
  • Capability checks for the caller token, an explicit token, or a token accessor.
  • Audit device list, enable, disable, and hash helpers.
  • Safe exact lease lookup, renew, and revoke helpers.
  • Plugin catalog list, type-list, register, read, delete, and backend reload helpers.
  • Explicitly gated production init, unseal, seal, rekey, key-share rotation, and keyring rotation operator APIs.
  • Environment-based client construction from common OpenBao/Vault variables.
  • Shared authenticated client and Rust Duration to OpenBao duration string helpers for async application ergonomics.
  • Bootstrap report lookup helpers for issued credentials and changed steps.
  • Raw JSON request escape hatch for endpoints that are not typed yet.
  • Typed custom plugin wrapper pattern documentation for application-specific OpenBao plugin APIs.
  • Local TLS OpenBao Podman stack on 9940 and 9941.
  • Real OpenBao integration test gate using the pinned OpenBao image.

Planned next:

  • Remaining 0.7.0: none.
  • 0.8.0: remaining auth methods and broader system backend automation.

See API Coverage and Release Plan for the road to 1.0.0.

Trust Dashboard

Area Status
License MIT OR Apache-2.0
Rust edition 2024
MSRV Rust 1.90.0
Async runtime Runtime-agnostic client; examples use Tokio
HTTP transport reqwest with redirects disabled
Default TLS backend Rustls
TLS floor TLS 1.3 by default; TLS 1.2 requires explicit opt-in
Plain HTTP Rejected by default; sensitive requests still require HTTPS
Token storage openbao::SecretString (secrecy::SecretString)
Unsafe policy unsafe_code = "forbid"
Path validation Rejects traversal, query/fragment injection, empty segments, controls, and trailing periods
Error posture API error strings are bounded and sanitized before formatting
Dependency policy cargo deny plus RustSec audit in the release gate
Release evidence fmt, clippy, tests, docs, deny, audit, SBOM, and real OpenBao integration
Pentest gate Required before tagging a release

Security details live in SECURITY.md. Release evidence and release sequencing live in release-notes and docs/RELEASE_PLAN.md.

Rust Version Support

The minimum supported Rust version is Rust 1.90.0. New deployments should prefer the latest stable Rust; as of June 1, 2026, that is Rust 1.96.0.

The 0.7.0 development line tracks compatibility evidence across this supported range:

Rust Required Evidence
1.90.0 Full test suite and clippy.
1.91.0 cargo check --all-features.
1.92.0 cargo check --all-features.
1.93.0 cargo check --all-features.
1.94.0 cargo check --all-features.
1.95.0 cargo check --all-features.
1.96.0 cargo check --all-features.

Install

[dependencies]
openbao = "0.7"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread"] }

Some advanced examples below use JSON helper types directly:

[dependencies]
serde_json = "1.0.150"

The crate defaults to the common SDK surface:

[dependencies]
openbao = { version = "0.7", features = ["approle", "cert-auth", "cubbyhole", "database", "jwt-auth", "kubernetes-auth", "kubernetes", "userpass", "token", "kv1", "kv2", "pki", "ssh", "totp", "transit", "sys", "rustls-tls"] }

For a smaller build, disable defaults and opt into only what the application uses:

[dependencies]
openbao = { version = "0.7", default-features = false, features = ["kv2", "sys", "rustls-tls"] }

Features

Feature Default Purpose
approle yes AppRole login, role, RoleID, and SecretID helpers.
cert-auth yes TLS certificate auth login/config/role/CRL helpers.
cubbyhole yes Token-scoped Cubbyhole read/write/delete/list helpers.
database yes Database secrets engine config, role, credential, and rotation helpers.
identity yes Identity entity, group, entity-alias, and group-alias helpers.
jwt-auth yes JWT login plus JWT/OIDC config and role administration helpers.
kubernetes-auth yes Kubernetes auth login/config/role helpers.
kubernetes yes Kubernetes secrets engine config, role, and generated service account token helpers.
ldap yes LDAP secrets engine config, static/dynamic role, credential, and library helpers.
rabbitmq yes RabbitMQ secrets engine connection, lease, role, and credential helpers.
userpass yes Userpass login and user administration helpers.
token yes Token lifecycle helpers.
kv1 yes KV v1 secrets engine helpers.
kv2 yes KV v2 secrets engine helpers.
pki yes PKI authority, issuer/key metadata/import, role, issue/sign, revoke, cert read/list, ACME config/EAB/directory URL, CRL config/rotate, and tidy helpers.
ssh yes SSH roles, OTP credentials, issuer management, CA sign/issue, issuer config, and OTP verification helpers.
totp yes TOTP key and code helpers.
transit yes Transit cryptography helpers.
transit-bytes no Raw-byte Transit convenience helpers using base64-ng for OpenBao's base64 request/response fields.
sys yes System backend helpers.
allow-sha1 no Explicit opt-in for legacy Transit SHA-1 selection. Disabled by default.
rustls-tls yes Rustls transport configuration.
native-tls no Legacy native TLS support. Requires native-tls-acknowledged after audit.
native-tls-acknowledged no Explicit acknowledgment for audited native TLS builds.
operator-ops no Production init, unseal, seal, rekey, key-share rotate, and keyring rotate APIs. Requires operator-ops-acknowledged.
operator-ops-acknowledged no Explicit acknowledgment for audited operator-operation builds.

Support Matrix

Client, Transport, And TLS

Capability Status Notes
Async client Yes Built on reqwest with a small public API surface.
Typestate auth Yes Separate unauthenticated and authenticated client states.
HTTPS by default Yes Plain HTTP is rejected unless loopback HTTP is explicitly enabled.
Redirect protection Yes Redirect following is disabled to avoid forwarding token headers.
Response size cap Yes 32 MiB default with per-client lowering for small-response workflows.
TLS floor Yes TLS 1.3 minimum by default; audited legacy deployments can opt down to TLS 1.2.
Custom CA roots Yes Extra root certificates can be merged with the platform trust store.
Root-only trust stores Yes System roots can be bypassed by using only configured root certificates.
Client TLS identity Yes Optional mutual TLS client identity for TLS certificate auth.
Connection timeout Yes 5-second connection timeout by default; caller overrides are bounded.
User agent fingerprinting Yes Default user agent omits the exact crate version.
Namespace header Yes X-Vault-Namespace support for namespace-aware deployments.
Environment construction Yes Reads OPENBAO_*, BAO_*, and VAULT_* aliases with secure defaults.
Raw JSON requests Yes Escape hatch for endpoints that are not typed yet.

Authentication

Capability Status Notes
Direct token auth Yes Tokens are accepted as SecretString.
X-Vault-Token Yes Default documented OpenBao-compatible token header.
Bearer auth Yes Optional Authorization: Bearer header mode.
AppRole login/admin Yes Role ID, SecretID, accessors, and returned tokens are secret-aware; role and SecretID lifecycle helpers are typed.
Token accessor handling Yes Accessors are treated as secret material.
Token lifecycle helpers Yes Lookup, accessor lookup/list, renew, revoke, revoke-self, and create helpers.
Kubernetes auth Yes Login, auth method config, and role administration helpers.
TLS certificate auth Yes Login, auth method config, CA role administration, and CRL helpers.
JWT/OIDC Yes JWT login plus JWT/OIDC auth method config and role administration helpers. Browser OIDC callback helpers are still planned.
Userpass auth Yes Login and user create/read/list/delete, password update, and policy update helpers.

Secret Engines

Capability Status Notes
KV v2 read/write Yes Typed serialization and deserialization.
KV v2 CAS write Yes Optional check-and-set version support.
KV v2 patch Yes JSON merge patch content type.
KV v2 list/delete versions Yes Metadata list, latest delete, soft delete, undelete, and destroy.
KV v2 metadata/config Yes Backend, per-key metadata, typed data, and secret-aware service config helpers.
KV v1 Yes Read, write, delete, and list helpers.
Cubbyhole Yes Token-scoped read, optional read, write, delete, and list helpers.
Kubernetes secrets Yes Config, role create/read/list/delete, and generated service account token helpers.
RabbitMQ secrets Yes Connection config, lease config, role create/read/list/delete, and generated credential helpers.
Identity Yes Entity, group, entity-alias, and group-alias lifecycle helpers.
LDAP secrets Yes Config, root rotation, static roles/credentials, dynamic roles/credentials, and library check-out/check-in helpers.
Database credentials Yes Connection config/list/read/delete, dynamic roles/credentials, static roles/credentials, and root/static rotation helpers.
Transit Yes Key create/read/list/delete, encrypt, decrypt, rewrap, data key, random, hash, HMAC, sign, verify, typed RSA/JWS signing options, and optional raw-byte helpers.
PKI Partial Authority generation/signing/install, URL/CRL config, roles, issue, sign, revoke, certificate list/read, issuer/key list/read/delete/update, issuer revoke, CA/key import, ACME config/EAB/directory URL, CRL rotate, and tidy are implemented.
TOTP Yes Key create/read/list/delete, code generation, and code validation helpers.
SSH Partial Roles, zero-address roles, IP role lookup, OTP credentials, issuer config/list/submit/read/update/delete, authenticated CA public-key metadata, CA sign/issue, and OTP verification are implemented. Raw unauthenticated public-key reads are intentionally not typed.
Custom plugin patterns Yes Documented wrapper pattern for typed plugin-specific APIs over Client::request_json.

System Backend And Operations

Capability Status Notes
Health Yes Accepts OpenBao active, standby, sealed, and uninitialized health statuses.
Init status Yes Typed /sys/init status helper.
Seal status Yes Typed /sys/seal-status helper.
Dev bootstrap Yes Fresh numeric-loopback dev instances only; not for production or HSM/KMS deployments.
Mount management Yes Secret and auth mount enable/list/read/tune/disable helpers.
Response wrapping Yes Lookup, wrap, unwrap, and rewrap helpers.
Policies and capabilities Yes ACL policy read/write/list/delete, bounded policy builder helpers, and self/token/accessor capability checks.
Admin bootstrap Yes Idempotent plan builder for mounts, Transit keys, ACL policies, KV v2 string values, auth methods, AppRole roles, explicit token issuance, and explicit AppRole SecretID issuance.
Audit devices Yes Enable, list, disable, and audit hash helpers.
Lease helpers Yes Safe exact lookup, renew, and revoke; prefix/force/tidy operations are intentionally not exposed.
Plugin catalog Yes List, type-list, register, read, delete, and mounted backend reload helpers.
Production init, unseal, rekey, rotate Gated Available only with operator-ops plus operator-ops-acknowledged; default builds cannot call these APIs.
Quotas, metrics, namespaces Planned Planned in the 0.8.0 operations line.

Examples

Create a client from an existing token:

use openbao::{Client, Result};
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let health = client.sys().health().await?;
    println!("openbao version: {}", health.version);
    Ok(())
}

Create an authenticated client from environment variables:

use openbao::{Client, Result};

#[tokio::main]
async fn main() -> Result<()> {
    // Reads OPENBAO_ADDR/BAO_ADDR/VAULT_ADDR plus token, namespace, and CA aliases.
    let client = Client::from_env_with_token()?;

    let health = client.sys().health().await?;
    println!("openbao version: {}", health.version);
    Ok(())
}

Configure a stricter client with a namespace and root-only trust store:

use openbao::{Client, OpenBaoConfig, Result};
use openbao::Certificate;
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let ca_pem = std::fs::read("openbao-ca.pem").map_err(|_| {
        openbao::Error::InvalidTlsConfig(
            "failed to read the configured CA certificate file".into(),
        )
    })?;
    let ca = Certificate::from_pem(&ca_pem)?;

    let config = OpenBaoConfig::new("https://bao.example.com:8200")?
        .namespace("admin/team-a")?
        .only_root_certificates(vec![ca])?;

    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::from_config(config)?.try_with_token(token)?;

    let seal = client.sys().seal_status().await?;
    println!("sealed: {}", seal.sealed);
    Ok(())
}

Authenticate with AppRole:

use openbao::{Client, Result};
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let client = Client::new("https://bao.example.com:8200")?;
    let role_id = SecretString::from(std::env::var("APPROLE_ROLE_ID").unwrap_or_default());
    let secret_id = SecretString::from(std::env::var("APPROLE_SECRET_ID").unwrap_or_default());

    let (client, login) = client.login_approle(role_id, secret_id).await?;
    let health = client.sys().health().await?;

    let _token_accessor = login.accessor;
    println!("openbao version: {}", health.version);
    Ok(())
}

Authenticate with Userpass:

use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let client = Client::new("https://bao.example.com:8200")?;
    let password = SecretString::from(std::env::var("BAO_USERPASS_PASSWORD").unwrap_or_default());

    let (client, login) = client.login_userpass("alice", password).await?;
    let health = client.sys().health().await?;

    let _token_accessor = login.accessor;
    println!("openbao version: {}", health.version);
    Ok(())
}

Authenticate with JWT:

use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let client = Client::new("https://bao.example.com:8200")?;
    let jwt = SecretString::from(std::env::var("SERVICE_JWT").unwrap_or_default());

    let (client, login) = client.login_jwt(Some("web"), jwt).await?;
    let health = client.sys().health().await?;

    let _token_accessor = login.accessor;
    println!("openbao version: {}", health.version);
    Ok(())
}

Write and read KV v2 data:

use openbao::{Client, Result};
use openbao::SecretString;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct DatabaseCredentials {
    username: String,
    password: SecretString,
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let kv = client.kv2("secret")?;

    kv.write(
        "production/database",
        DatabaseCredentials {
            username: "app".to_owned(),
            password: SecretString::from("change-me"),
        },
    )
    .await?;

    let secret = kv
        .read::<DatabaseCredentials>("production/database")
        .await?;

    let _username = secret.data.username;
    let _password = secret.data.password;
    println!("database credentials loaded");
    Ok(())
}

Use KV v2 check-and-set, patch, and version operations:

use openbao::secrets::kv2::{Kv2MetadataOptions, Kv2WriteOptions};
use openbao::{Client, Result};
use openbao::SecretString;
use serde::Serialize;

#[derive(Serialize)]
struct Patch {
    password: SecretString,
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let kv = client.kv2("secret")?;

    kv.write_with_options(
        "app/config",
        serde_json::json!({ "username": "app", "password": "first" }),
        Some(Kv2WriteOptions { cas: Some(0) }),
    )
    .await?;

    let second = kv
        .patch("app/config", Patch {
            password: SecretString::from("rotated"),
        })
        .await?;

    let _previous = kv
        .read_version::<serde_json::Value>("app/config", second.version - 1)
        .await?;
    kv.delete_versions("app/config", &[second.version - 1]).await?;
    kv.undelete_versions("app/config", &[second.version - 1]).await?;
    kv.destroy_versions("app/config", &[second.version - 1]).await?;

    kv.patch_metadata(
        "app/config",
        &Kv2MetadataOptions {
            max_versions: Some(10),
            cas_required: Some(true),
            delete_version_after: Some("24h".to_owned()),
            custom_metadata: None,
        },
    )
    .await?;

    Ok(())
}

Load service configuration from KV v2:

use openbao::{Client, Result};
use openbao::SecretString;
use serde::Deserialize;

#[derive(Deserialize)]
struct AppConfig {
    database_url: SecretString,
    listen_addr: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let kv = client.kv2("secret")?;

    let typed = kv.read_data::<AppConfig>("services/api").await?;
    let env_map = kv.read_service_config("services/api-env").await?;
    kv.write_service_config("services/api-env-copy", &env_map).await?;

    println!("listen: {}", typed.listen_addr);
    println!("loaded {} secret config keys", env_map.len());
    let _database_url_is_not_logged = typed.database_url;
    Ok(())
}

Share an authenticated client across async tasks:

use openbao::{Client, Result, SecretString, SharedClient};

fn worker_client(token: SecretString) -> Result<SharedClient> {
    Ok(Client::new("https://bao.example.com:8200")?
        .try_with_token(token)?
        .into_shared())
}

Read dynamic database credentials:

use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let database = client.database("database")?;

    let credentials = database.credentials("readonly").await?;
    let _password = credentials.password;
    println!(
        "database user {} leased for {} seconds",
        credentials.username, credentials.lease_duration
    );
    Ok(())
}

Create and validate a TOTP code without logging the generated code:

use openbao::secrets::totp::{TotpKeyCreateRequest, TotpValidateRequest};
use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let totp = client.totp("totp")?;

    let created = totp
        .create_key("alice", &TotpKeyCreateRequest::generated("Example", "alice"))
        .await?;
    println!("TOTP bootstrap URL available: {}", created.url.is_some());

    let code = totp.generate_code("alice").await?;
    let validation = totp
        .validate_code("alice", &TotpValidateRequest::new(code.code))
        .await?;

    println!("TOTP code accepted: {}", validation.valid);
    Ok(())
}

Issue an SSH certificate and generated private key without logging the key:

use openbao::secrets::ssh::{SshIssueKeyType, SshIssueRequest};
use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let ssh = client.ssh("ssh")?;

    let issued = ssh
        .issue("user-cert", &SshIssueRequest::new(SshIssueKeyType::Ed25519))
        .await?;

    println!("SSH certificate length: {}", issued.signed_key.len());
    let _private_key_is_not_logged = issued.private_key;
    Ok(())
}

Use a KV v1 mount:

use openbao::{Client, Result};
use openbao::SecretString;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Config {
    endpoint: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let kv = client.kv1("legacy-secret")?;

    kv.write("app/config", Config {
        endpoint: "https://api.example.com".to_owned(),
    })
    .await?;

    let config = kv.read::<Config>("app/config").await?;
    let keys = kv.list("app").await?;

    println!("endpoint: {}", config.endpoint);
    println!("keys: {}", keys.keys.len());
    Ok(())
}

Encrypt and decrypt through Transit:

use openbao::secrets::transit::{TransitDecryptRequest, TransitEncryptRequest};
use openbao::{Client, Result};
use openbao::{ExposeSecret, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let transit = client.transit("transit")?;

    let encrypted = transit
        .encrypt(
            "app-key",
            &TransitEncryptRequest::new(SecretString::from("c2VjcmV0")),
        )
        .await?;

    let decrypted = transit
        .decrypt(
            "app-key",
            &TransitDecryptRequest::new(encrypted.ciphertext),
        )
        .await?;

    println!("plaintext length: {}", decrypted.plaintext.expose_secret().len());
    Ok(())
}

Encrypt and decrypt raw bytes with the optional transit-bytes feature:

use openbao::secrets::transit::{TransitDecryptRequest, TransitEncryptRequest};
use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let transit = client.transit("transit")?;

    let request = TransitEncryptRequest::from_plaintext_bytes(b"secret")?;
    let encrypted = transit.encrypt("app-key", &request).await?;
    let decrypted = transit
        .decrypt("app-key", &TransitDecryptRequest::new(encrypted.ciphertext))
        .await?;

    let plaintext = decrypted.plaintext_bytes()?;
    println!("plaintext length: {}", plaintext.len());
    Ok(())
}

Sign data for JWS/JWT-style ECDSA workflows:

use openbao::secrets::transit::{
    TransitHashAlgorithm, TransitSignRequest, TransitVerifyRequest,
};
use openbao::{Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;
    let transit = client.transit("transit")?;

    let input = SecretString::from("ZXhhbXBsZS1wYXlsb2Fk");
    let signed = transit
        .sign(
            "jwt-signing-key",
            Some(TransitHashAlgorithm::Sha2_256),
            &TransitSignRequest::jws(input.clone()),
        )
        .await?;

    let verified = transit
        .verify(
            "jwt-signing-key",
            Some(TransitHashAlgorithm::Sha2_256),
            &TransitVerifyRequest::jws_with_signature(input, signed.signature),
        )
        .await?;

    println!("signature valid: {}", verified.valid);
    Ok(())
}

Create, inspect, renew, and revoke a child token:

use openbao::auth::token::TokenCreateRequest;
use openbao::{Client, Result};
use openbao::SecretString;
use std::collections::BTreeMap;

#[tokio::main]
async fn main() -> Result<()> {
    let root_or_parent = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(root_or_parent)?;

    let child = client
        .token()
        .create(
            &TokenCreateRequest {
                meta: BTreeMap::from([("owner".to_owned(), "example".to_owned())]),
                display_name: Some("example-child".to_owned()),
                renewable: Some(true),
                ..TokenCreateRequest::default()
            }
            .with_policies(["default"])
            .with_ttl("30m")?
            .with_explicit_max_ttl("1h")?,
        )
        .await?;

    let info = client.token().lookup(&child.client_token).await?;
    println!("renewable: {}", info.renewable);

    let _renewed = client.token().renew(&child.client_token, Some("15m")).await?;
    client.token().revoke_accessor(&child.accessor).await?;
    Ok(())
}

Enable a KV v2 mount:

use openbao::{Client, Result};
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    client
        .sys()
        .enable_kv2("apps", Some("application secrets"))
        .await?;

    let mounts = client.sys().list_mounts().await?;
    println!("mount count: {}", mounts.len());
    Ok(())
}

Wrap and unwrap JSON data:

use openbao::{Client, Result};
use openbao::SecretString;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct WrappedPayload {
    nonce: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let wrap = client
        .sys()
        .wrapping_wrap("5m", &WrappedPayload {
            nonce: "one-time".to_owned(),
        })
        .await?;

    let payload = client
        .sys()
        .wrapping_unwrap::<WrappedPayload>(Some(&wrap.token))
        .await?;

    println!("payload nonce length: {}", payload.nonce.len());
    Ok(())
}

Write an ACL policy and check capabilities:

use openbao::{AclPolicyBuilder, Client, Result, SecretString};

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let mut policy = AclPolicyBuilder::new();
    let request = policy
        .allow_kv2_read_prefix("secret", "app")?
        .build_write_request()?;

    client
        .sys()
        .write_policy("app-read", &request)
        .await?;

    let capabilities = client.sys().capabilities_self(["secret/data/app"]).await?;
    let _for_path = capabilities.by_path.get("secret/data/app");
    Ok(())
}

Run a small idempotent service bootstrap:

use openbao::auth::token::TokenCreateRequest;
use openbao::bootstrap::AdminBootstrap;
use openbao::secrets::transit::TransitCreateKeyRequest;
use openbao::{AclPolicyBuilder, Client, Result, SecretString};
use std::collections::BTreeMap;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let mut policy = AclPolicyBuilder::new();
    policy.allow_kv2_read_prefix("secret", "app")?;

    let mut values = BTreeMap::new();
    values.insert("API_KEY".to_owned(), SecretString::from(std::env::var("APP_API_KEY").unwrap_or_default()));

    let mut bootstrap = AdminBootstrap::new();
    let report = bootstrap
        .ensure_kv2_mount("secret", Some("application secrets"))?
        .ensure_transit_mount("transit", Some("application crypto"))?
        .ensure_policy("app-read", &policy)?
        .ensure_transit_key("transit", "app-key", TransitCreateKeyRequest::default())?
        .ensure_kv2_secret_values("secret", "app/config", values)?
        .issue_service_token(
            "app",
            TokenCreateRequest::default()
                .with_policies(["app-read"])
                .without_default_policy()
                .with_ttl("1h")?,
        )?
        .run(&client)
        .await?;

    println!("bootstrap steps: {}", report.steps.len());
    let _issued_token_is_not_logged = report.issued_tokens;
    Ok(())
}

Enable an audit device and calculate an audit hash:

use openbao::{Client, Result, SecretString};
use openbao::sys::AuditEnableRequest;
use std::collections::BTreeMap;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    client
        .sys()
        .enable_audit_device(
            "file",
            &AuditEnableRequest {
                options: BTreeMap::from([(
                    "file_path".to_owned(),
                    "/var/log/openbao/audit.log".to_owned(),
                )]),
                ..AuditEnableRequest::new("file").with_description("local audit file")
            },
        )
        .await?;

    let hash = client
        .sys()
        .audit_hash("file", &SecretString::from("known-secret-value"))
        .await?;

    println!("audit hash prefix: {}", hash.hash.split(':').next().unwrap_or(""));
    Ok(())
}

Look up and renew one exact lease:

use openbao::{Client, Result};
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let lease_id = SecretString::from(std::env::var("BAO_LEASE_ID").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let lease = client.sys().lookup_lease(&lease_id).await?;
    if lease.renewable {
        let renewed = client.sys().renew_lease(&lease_id, Some(1800)).await?;
        println!("renewed lease seconds: {}", renewed.lease_duration);
    }

    Ok(())
}

Read and reload a plugin catalog entry:

use openbao::sys::{PluginReloadRequest, PluginType};
use openbao::{Client, Result};
use openbao::SecretString;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let plugins = client.sys().list_plugins_by_type(PluginType::Secret).await?;
    if plugins.keys.iter().any(|name| name == "transit") {
        let _info = client
            .sys()
            .read_plugin(PluginType::Secret, "transit", None)
            .await?;
    }

    client
        .sys()
        .reload_plugin_backend(&PluginReloadRequest {
            plugin: Some("example-plugin".to_owned()),
            mounts: Vec::new(),
            scope: None,
        })
        .await?;

    Ok(())
}

Call an endpoint that is not typed yet:

use openbao::{Client, Result};
use openbao::Method;
use openbao::SecretString;
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<()> {
    let token = SecretString::from(std::env::var("BAO_TOKEN").unwrap_or_default());
    let client = Client::new("https://bao.example.com:8200")?.try_with_token(token)?;

    let response: Value = client
        .request_json(Method::GET, "sys/internal/specs/openapi", Option::<&Value>::None)
        .await?;

    println!("openapi keys: {}", response.as_object().map_or(0, |object| object.len()));
    Ok(())
}

The raw request layer is intentionally low level. Prefer typed helpers when the crate supports an endpoint; use raw JSON to bridge missing OpenBao APIs while coverage grows.

For application-specific OpenBao plugins, keep raw JSON calls behind a small typed wrapper. See Typed Custom Plugin Pattern for a complete request/response wrapper example with path validation, secret redaction, and test guidance.

Local OpenBao Dev Instance

The local dev stack uses Podman, TLS, a private CA, and loopback-only ports in the requested 994x range.

Prepare the rootless Podman volume and TLS assets without starting OpenBao:

scripts/openbao_dev.sh prepare

This project does not require a /srv directory tree for local development: raft data lives in a Podman-managed volume, and TLS material lives in the ignored deploy/podman/dev-state/ directory.

scripts/openbao_dev.sh up

Endpoints:

  • API: https://127.0.0.1:9940
  • Cluster: https://127.0.0.1:9941
  • CA certificate: deploy/podman/dev-state/tls/dev-ca.crt

Initialize and unseal OpenBao using bao operator init and bao operator unseal, then export BAO_ADDR=https://127.0.0.1:9940 and BAO_CACERT=deploy/podman/dev-state/tls/dev-ca.crt.

For disposable local development, the crate can initialize and unseal a fresh numeric-loopback instance directly:

use openbao::{Client, OpenBaoConfig, Result, sys::DevBootstrapOptions};
use openbao::Certificate;

#[tokio::main]
async fn main() -> Result<()> {
    let ca_pem = std::fs::read("deploy/podman/dev-state/tls/dev-ca.crt").map_err(|_| {
        openbao::Error::InvalidTlsConfig(
            "failed to read the configured CA certificate file".into(),
        )
    })?;
    let ca = Certificate::from_pem(&ca_pem)?;

    let config = OpenBaoConfig::new("https://127.0.0.1:9940")?
        .only_root_certificates(vec![ca])?;
    let client = Client::from_config(config)?;

    let bootstrap = client
        .sys()
        .bootstrap_dev(&DevBootstrapOptions::single_key())
        .await?;

    let health = bootstrap.client.sys().health().await?;
    println!("initialized: {}, sealed: {}", health.initialized, health.sealed);
    Ok(())
}

bootstrap_dev is not a production initialization ceremony. It creates root and unseal material in process memory, uses Shamir keys, refuses non-loopback targets, and refuses already initialized servers. Do not use it with HSM/KMS auto-unseal, shared environments, or any instance that requires operator key ceremony.

Run the real OpenBao integration flow:

scripts/openbao_integration.sh

The integration script creates a fresh TLS dev instance, initializes and unseals it, stores the root token in a temporary 0600 file for the test process, and removes that file when the run exits.

Release Discipline

Run the normal local checks:

scripts/checks.sh

Run the current release gate:

scripts/release_0_7_gate.sh

Set OPENBAO_SKIP_INTEGRATION=1 only when Podman is unavailable; release candidate validation should run the integration gate.

No release tag should be cut unless the matching pentest report status is reviewed and recorded in the release notes.