A self-hosted secrets server is a small service that holds your homelab's passwords, API keys and certificates encrypted at rest, hands them out only to authenticated machines and people, logs every request, and can mint short lived credentials on demand. OpenBao, the open source project now hosted by the OpenSSF, is the most direct way to run one at home. You need it once encrypted files in Git stop answering "who read this, and when does it expire".
That is the short answer. This post goes one level deeper than our guide to Git native secrets with age and SOPS. It explains how OpenBao's seal works, what an unseal key share actually protects, how machines and humans log in, how dynamic database credentials and the PKI engine work, and then walks through a full deployment on a single VM: initialise, store the shares, enable engines, write a least privilege policy, rotate a secret, take a snapshot and prove that the snapshot restores. Every OpenBao behaviour below comes from the project's own documentation and changelog, read at release 2.7.1. Key management principles come from NIST SP 800-57 Part 1 Revision 5 and the OWASP Secrets Management Cheat Sheet.
Key takeaways
- OpenBao is an identity based secrets and encryption management system. It is licensed under the Mozilla Public License 2.0, reached its first GA release (2.0.0) on 16 July 2024, moved from LF Edge to the OpenSSF as a sandbox project in June 2025, and shipped 2.7.1 on 1 October 2026.
- Data in OpenBao is encrypted three layers deep: the data is encrypted by keys in a keyring, the keyring by a root key, and the root key by an unseal key. The default Shamir seal splits that unseal key into 5 shares with a threshold of 3.
- A sealed server can read its storage but cannot decrypt it. It stays unsealed until it is resealed, restarted, or its storage layer hits an unrecoverable error, so every reboot of a Shamir sealed server needs a quorum of shares.
- The initial root token never expires. The project's own guidance is to use it only for initial setup or emergencies and revoke it immediately afterwards, then mint new root tokens with
bao operator generate-rootand a quorum of share holders when one is truly needed. - Dynamic secrets are the main thing a secrets server adds over encrypted files. A database role creates a new login per request with a lease (for example one hour), and OpenBao drops the login when the lease ends or is revoked.
- Audit devices are off on a fresh server, and since 2.3.2 they cannot be created through the API by default. Declare them in the server configuration file, and run at least two because OpenBao refuses requests it cannot log anywhere.
- Backups are only half of the job. A Raft snapshot is encrypted with the same keys, so a backup without the unseal shares (or the auto unseal key) is worthless, and a backup you have never restored is a hope rather than a plan.
- NIST SP 800-57 suggests a cryptoperiod of under two years for symmetric data encryption and key wrapping keys. OpenBao rotates its data encryption key automatically before about 2 to the power 32 encryptions; rotating the unseal shares and the encryption key on a schedule is still your job.
When does a homelab outgrow encrypted files in Git?
Encrypted files in Git are a good first step. With SOPS and age, every secret lives in the same repository as the configuration that uses it, diffs show which key changed, and decryption needs nothing more than a private key on the machine doing the deploy. SOPS was started at Mozilla in 2015, became a CNCF sandbox project in 2023 and is licensed under MPL 2.0. age describes itself as a simple, modern and secure file encryption tool with small explicit keys and no configuration options. For a lab with a handful of services and one operator, that pair is often enough.
The trouble starts when the questions change. Four signs that you have outgrown files:
- You cannot answer "who read it". Git records who changed a secret, not who decrypted it. Anyone holding the age private key can decrypt every file encrypted to that key, silently, as often as they like.
- Rotation is a manual project. Changing a database password means generating a new one, editing the encrypted file, committing, redeploying every consumer and then changing it in the database, in the right order. In practice this means the password never changes.
- Secrets live forever. A token copied to a laptop for a quick test works until someone remembers to revoke it upstream. Nothing expires on its own.
- You want certificates and machine identity. Once you run your own certificate authority, issue short lived TLS certificates, or want each container to log in as itself, you need something that answers requests, not something that decrypts files.
The OWASP Secrets Management Cheat Sheet frames the same shift as centralising and standardising the secret lifecycle, including authentication, authorisation and accounting, and notes that standardising can still mean several solutions, each documented so it can be found during an incident. It also makes a point worth repeating for anyone who thinks a restart fixes a leak: "Restarting the application does not itself revoke stolen credentials". Only expiry or revocation at the provider does that.
What a secrets server adds
| Capability | Encrypted files in Git | Secrets server (OpenBao) |
|---|---|---|
| Read audit | No record of decryption | Every request and response logged by audit devices, with secret values HMAC hashed |
| Short lived credentials | Not possible; files hold static values | Dynamic secrets with leases that expire and are revoked automatically |
| Per consumer identity | Everyone with the key reads everything encrypted to it | Each machine or person logs in and gets a token bound to policies |
| PKI | Separate tool | PKI secrets engine issues certificates from your own CA, with ACME support |
| Rotation | Edit, commit, redeploy by hand | Write a new KV version; agents re-render; database root credentials can be rotated so only OpenBao knows them |
| Offline use | Works with no network | Consumers need the server reachable, or a cached copy |
| Failure mode | Lost private key means lost secrets | Lost unseal shares or auto unseal key means a locked server, including its backups |
The last two rows are the honest cost. A secrets server is a running dependency. If it is down, new credentials cannot be issued, and if it is sealed after a power cut, nothing that depends on it starts until someone brings a quorum of shares.
What is OpenBao?
OpenBao is "an identity-based secrets and encryption management system", in the words of its own documentation. A secret is anything you want to tightly control access to: API tokens, encryption keys, passwords and certificates. Clients reach it through a CLI called bao, an HTTP API, or a web UI, and every operation follows the same four stages the docs describe: the client authenticates, OpenBao validates it against a trusted source, matches it to policies, and then grants access to the paths those policies allow.
The headline features in the project's introduction are secure secret storage (data is encrypted before it is written, so raw storage access is not enough), dynamic secrets generated on demand and revoked when their lease ends, encryption as a service through the Transit engine, leasing and renewal for every secret, and revocation of single secrets or whole trees of them.
Where OpenBao came from
OpenBao is a community fork. When a widely used commercial secrets manager moved from the open source MPL 2.0 licence to a source available licence in 2023, a group of engineers forked the last MPL licensed code. Press reports at the time dated the public announcement to December 2023, and the OpenBao project's own blog confirms it was first developed under LF Edge, the Linux Foundation's edge computing umbrella. The OpenBao repository's licence file is still MPL 2.0, and the project says it intends to stay under an OSI approved licence with open governance.
| Date | Milestone | Source |
|---|---|---|
| December 2023 | Fork announced as a Linux Foundation project under LF Edge | Press reports at the time; LF Edge origin confirmed by the OpenBao blog |
| 29 March 2024 | 2.0.0 alpha release | OpenBao CHANGELOG |
| 16 July 2024 | 2.0.0, the first GA release (shipped without the built in web UI) | OpenBao CHANGELOG; release blog dated 25 July 2024 |
| 29 November 2024 | 2.1.0 | OpenBao CHANGELOG |
| 17 June 2025 | Joins the OpenSSF as a sandbox project, moving from LF Edge | OpenBao blog |
| 7 August 2025 | 2.3.2 disables audit device creation through the API by default | OpenBao CHANGELOG |
| 28 August 2025 | 2.4.0 adds bao operator rotate-keys | OpenBao docs and CHANGELOG |
| 14 July 2026 | 2.6.0 adds per namespace sealing and external KMS plugins for auto unseal | OpenBao CHANGELOG |
| 23 September 2026 | 2.7.0 adds PostgreSQL horizontal read scaling, external HSM and KMS backed keys, ML-DSA and ML-KEM, and control groups; removes the file storage backend | OpenBao CHANGELOG; release blog dated 5 October 2026 |
| 1 October 2026 | 2.7.1 and 2.6.4 security and bug fix releases | OpenBao CHANGELOG |
Two things in that table matter for a homelab. First, older tutorials written for the pre-fork code mostly still work, but command names and defaults have drifted: operator rekey is deprecated in favour of operator rotate-keys, mlock support and the disable_mlock option were removed in 2.0.0, and the file storage backend was removed in 2.7.0. Second, the project moves quickly and ships security fixes in point releases, so treat the OpenBao binary or container image like any other dependency you keep current.
How OpenBao's architecture works
Storage is untrusted
OpenBao's docs describe the storage backend as "untrusted storage used purely to keep encrypted information". Everything that leaves the OpenBao process is encrypted by the security barrier using AES-256 in Galois/Counter Mode with 96 bit nonces, and the GCM authentication tag is checked on every read, so tampering with storage is detected rather than silently accepted.
For a homelab there are three realistic backends in 2.7:
| Backend | High availability | Backups | When to pick it |
|---|---|---|---|
| Integrated Storage (Raft) | Yes, multi node with autopilot | Atomic snapshots with bao operator raft snapshot save | Default choice; one node now, three later without migrating |
| PebbleDB | No; never share one store between nodes | Offline backups (stop the service, copy the directory) | Single durable node; the default in the 2.7.1 Linux packages |
| PostgreSQL | Yes, with read scaling since 2.7 | Your PostgreSQL backup tooling | You already run and back up a PostgreSQL cluster |
The docs are blunt about backups: they are "ideally performed while OpenBao is offline", and if a backend does not support atomic snapshots you should only take offline backups. Raft is the only backend whose snapshot command is documented in OpenBao itself, which is why this guide uses it even on a single node.
The barrier: three layers of keys
The seal documentation explains the key hierarchy in one paragraph that is worth committing to memory:
- Most data is encrypted with the encryption key, which lives in a keyring stored alongside the data.
- The keyring is encrypted by the root key, also stored alongside the data.
- The root key is encrypted by the unseal key, which is not stored with the data at all.
Unsealing is the process of getting the plaintext root key into memory so the keyring, and through it the data, can be decrypted. Before that, OpenBao knows where its storage is and how to reach it, but almost nothing works: no logins, no mount table changes, only unseal and status calls.
This layering is what makes rotation cheap. Rotating the encryption key (bao operator rotate) adds a new key to the keyring; new writes use it and old data stays readable through older keys, with no re-encryption pass. Rotating the unseal shares (bao operator rotate-keys) generates a new root key, re-encrypts the keyring with it and hands out new shares, after which the old shares are useless. Both run online.
Shamir seals and key shares
The default seal is Shamir. Rather than giving one person a single unseal key, OpenBao uses Shamir's Secret Sharing to split it into shares, and any threshold number of shares can rebuild it. bao operator init defaults to 5 shares and a threshold of 3. Shares can be entered one at a time, in any order, from different machines, which is the point: no single person or location ever needs to hold the whole key.
NIST SP 800-57 Part 1 calls this split knowledge: a key is split into n shares, each of which gives no knowledge of the key, and if k shares are needed to rebuild it, any k minus 1 shares give no information about the key. That is the property you rely on when you leave one share with a friend and put another in a safe deposit box.
Three practical details from the docs that trip people up:
- In a multi node cluster, each node must be unsealed with the threshold of shares; partial unsealing is not shared across the cluster.
- A node stays unsealed until it is resealed through the API, the process restarts, or storage hits an unrecoverable error. Plan for every kernel update reboot.
- Sealing needs only one operator with root privileges and throws away the in memory root key. It is the emergency brake if you suspect an intrusion.
Auto unseal options for a homelab
The docs say plainly that Shamir makes automation hard and that, for most users, auto unseal is a better experience. Auto unseal hands the job of protecting the unseal key to a trusted device or service: at start up OpenBao asks that service to decrypt the root key it reads from storage. Initialising with auto unseal yields recovery keys instead of unseal keys. Recovery keys authorise quorum operations such as generating a root token, but they cannot decrypt the root key.
The warning attached to this is the most important sentence in the seal docs: if the seal mechanism or its keys are permanently deleted, the OpenBao cluster cannot be recovered, even from backups.
What is realistic at home:
| Seal | How it works | Homelab fit | Main risk |
|---|---|---|---|
| Shamir (default) | Split unseal key, quorum typed in after each start | Best for one node you rarely reboot | Every restart needs a person with shares |
| Transit | A second OpenBao server's Transit engine wraps the root key | Two node labs; a small "unsealer" instance | The unsealer itself needs unsealing; circular dependencies |
| Static key | A 32 byte AES-256-GCM key in config, an environment variable or a file | Only when another secret store already protects that key | Anyone who reads the key file and a backup has your data |
| PKCS#11 plugin | An HSM or token holds the key; external plugin since 2.7 | Labs with a hardware security token | Losing or wiping the token locks the server |
| Cloud KMS plugins | A cloud key management service holds the key; external plugins since 2.7 | Labs that already trust a cloud account | Account loss or key deletion is unrecoverable |
The static seal deserves a word of caution because it looks convenient. The docs recommend it only "when an existing source of trust, such as another third-party secrets manager, already exists in the operating environment". A static key in a file on the same disk as the Raft data turns the barrier into a formality: one stolen disk image contains both the ciphertext and the key. It does support n-1 to n rotation with previous_key and current_key fields, which is useful if you do chain it to a hardware token or a separate machine.
How auth methods work for machines and humans
Every client ends up holding a token. A token carries policies and a TTL, and every lease a token creates is revoked when the token is revoked. Auth methods are the different ways of trading some proof of identity for a token.
AppRole for machines and scripts
AppRole is built for automated workflows. A role has a RoleID, which works like a user name and is not secret, and a SecretID, which is secret and can be limited in lifetime, number of uses and source address. The docs' example role sets secret_id_ttl=10m, token_ttl=20m and secret_id_num_uses=40, and secret_id_bound_cidrs restricts logins to given address ranges.
The pattern that works at home: bake the RoleID into the host's configuration, deliver the SecretID once at provisioning time (for example from your configuration management run), and let the OpenBao Agent read it from a file and delete the file after reading. remove_secret_id_file_after_reading defaults to true in the agent's AppRole auto auth method.
OIDC for people
The JWT/OIDC method lets people log in through an identity provider you already run, such as a self-hosted single sign on server (see our homelab single sign on guide). With the default client callback mode, the CLI listens locally and the redirect URI is usually http://localhost:8250/oidc/callback; the UI and direct callback modes use https://<host:port>/v1/auth/<path>/oidc/callback. Map groups from the identity provider to OpenBao policies so that adding someone to the "lab-admins" group is the only step needed to give them access, and removing them is the only step needed to take it away.
Kubernetes auth for pods
If your lab runs k3s or another Kubernetes distribution, the Kubernetes auth method lets a pod log in with its service account token. A role binds bound_service_account_names and bound_service_account_namespaces to policies, so a pod in the default namespace running as myapp gets only what that role grants. This beats mounting a long lived token into every pod.
Userpass and the token method
Userpass is fine as a break glass login for one admin account stored in your password manager. Avoid it for daily use if OIDC is available, because it gives you a second password lifecycle to manage. Direct bao token create is for testing and for minting narrowly scoped tokens, not for day to day access.
Writing policies: least privilege paths
Policies are written in HCL and are deny by default: an empty policy grants nothing. Each path stanza lists capabilities: create, read, update, patch, delete, list, scan, sudo and deny. A trailing * is a prefix glob and + matches one path segment. When several patterns match, the most specific one wins, and the same pattern appearing in several policies has its capabilities unioned. deny always wins.
Three rules keep homelab policies tight:
- One policy per consumer. A Nextcloud container gets
nextcloud-read, nothing else. - Name the KV v2 sub paths. KV version 2 puts data under
data/and versions metadata undermetadata/. A reader needsreadonkv/data/apps/nextcloud/*, not onkv/*. - Keep
sys/out of application policies. Mount, policy and audit management paths belong to the admin role alone.
# nextcloud-read.hcl: everything the Nextcloud host may do
path "kv/data/apps/nextcloud/*" {
capabilities = ["read"]
}
path "database/creds/nextcloud" {
capabilities = ["read"]
}
# Let the agent renew and look up its own token
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
Root tokens: set up, then revoke
bao operator init returns an initial root token with no expiry. OpenBao's token docs say root tokens should be used only for "just enough initial setup" or in emergencies and revoked immediately after. If you need one later, bao operator generate-root creates one with the consent of a quorum of share holders, and the docs recommend multiple eyes on the terminal whenever a root token is live. At home, the second pair of eyes may just be a written checklist, but the revoke step is not optional.
What are dynamic secrets?
A dynamic secret does not exist until someone asks for it. When an authorised client reads database/creds/nextcloud, OpenBao connects to PostgreSQL with its own privileged account, runs the role's creation_statements to create a brand new login with a random password and a VALID UNTIL time, and returns the user name and password with a lease. The documentation's sample role uses default_ttl="1h" and max_ttl="24h", and the response looks like this:
Key Value
--- -----
lease_id database/creds/my-role/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6
lease_duration 1h
lease_renewable true
password SsnoaA-8Tv4t34f41baD
username v-openbaouse-my-role-x
When the lease ends, or someone runs bao lease revoke, OpenBao deletes the login. Revoking a token revokes every lease it created, and bao lease revoke -prefix database/creds/ revokes everything a mount has issued, which is the fastest incident response step a homelab can have: one command and every database login handed out by OpenBao stops working.
Leases and TTLs
Every secret in OpenBao has a lease. The server wide default_lease_ttl and max_lease_ttl both default to 768 hours (32 days), and mounts and roles can set shorter values. Leases can be renewed up to the maximum, after which the client must request a new credential. Two settings matter at home:
- Keep database roles short (an hour) and let the agent renew them. A leaked credential then has hours of life, not months.
- Set
max_lease_ttlon mounts that issue certificates or long lived tokens so a mistake in one role cannot hand out a ten year credential.
Static roles and root credential rotation
For services that cannot handle changing user names, the database engine also supports static roles: a one to one mapping between an OpenBao role and an existing database user whose password OpenBao rotates on a schedule. The docs warn not to use static roles for the root database credentials used in the connection config/, because rotation does not distinguish them. Instead, rotate the connection's root credential with database/rotate-root/<name>, after which only OpenBao knows the password. Make sure you have another way into the database before you do that.
The PostgreSQL plugin supports root credential rotation, dynamic roles, static roles and user name templates. The database engine also has plugins for MySQL and MariaDB, Valkey, InfluxDB and Cassandra in the 2.7 docs.
How the PKI engine pairs with a homelab CA
If you already run a home certificate authority (our guide to running your own CA at home covers the trust store side), the clean design is to keep the root offline and let OpenBao act as an intermediate. The PKI quick start shows both halves:
- Enable a mount for the intermediate, for example
pki_int, and raise its maximum TTL (bao secrets tune -max-lease-ttl=43800h pki_int, five years). - Generate a CSR inside OpenBao with
pki_int/intermediate/generate/internal. The private key never leaves the server. - Sign that CSR with your offline root, using whatever tool holds the root key.
- Import the signed certificate with
pki_int/intermediate/set-signed. - Create roles that limit which names can be issued (
allowed_domains,allow_subdomains) and set short TTLs. - Issue certificates with
pki_int/issue/<role>, or let clients use ACME.
The PKI considerations page has a section on automating rotation with ACME and a specific warning: if a role restricts what can be issued, require External Account Binding (eab_policy=always-required), because otherwise any ACME client that can reach the server can ask for certificates. On a flat home network that is everyone on the Wi-Fi.
The OpenBao server's own TCP listener can also obtain its certificate through ACME. The listener docs note that ACME is enabled by default when tls_cert_file is empty, pointing at a public CA's production directory, so if you mean to use your own certificate, set tls_cert_file explicitly, and if you mean to use your own ACME CA, set tls_acme_ca_directory.
How to run it safely
TLS on every listener
The OWASP cheat sheet's rule is simple: use TLS for all secret transmission. OpenBao's listener defaults to tls_min_version = "tls12". Set it to tls13 if every client supports it, issue the server certificate from your own CA, and never set tls_disable = true outside a throwaway dev box. Since 2.7 the listener also supports the post quantum ML-KEM key exchange and ML-DSA certificates according to the release notes; that is interesting but not required for a homelab.
Disable swap for the process
OpenBao removed mlock support in 2.0.0. The install guide instead asks you to make sure swap is disabled or encrypted. The example systemd unit shipped with the OpenBao source disables swap for the process with MemorySwapMax=0, which you can confirm with systemctl cat openbao; for containers the docs say to pass --memory-swappiness=0.
Integrated Raft storage
Use Raft with a node_id and a data path on local disk, not on a network share. One node is fine to start. If you later add two more, they join with retry_join and you gain the ability to reboot one node at a time, but remember that with Shamir each node needs its own quorum of shares on every start.
Audit devices
A fresh server has no audit device. Since 2.3.2, creating audit devices through the API is disabled unless unsafe_allow_api_audit_creation is set, a change made in response to CVE-2025-6000 and CVE-2025-54997. The supported way is a declarative audit stanza in the server configuration, reloaded on restart or SIGHUP.
Two points from the audit docs matter for design. OpenBao treats a request as successful if it can log to at least one device, and if it cannot log anywhere it refuses the request, so configure two devices (for example a file and syslog). And the log contains full requests and responses with most strings HMAC-SHA256 hashed; integers, booleans and timestamps pass through in plain text. Do not set log_raw, which the operator quick start shows in an example: it logs sensitive values without hashing. The OWASP cheat sheet asks for audit logs that record who requested what, protected against tampering, with accurate timestamps. Ship the file to your log server, not just to the disk next to the data.
Backups and snapshot restore tests
The storage docs ask "why take backups?" before telling you how. Their answers: before upgrades (downgrading storage is not always possible), before major changes such as rotations, and to recover from accidental deletion, with the caveat that restoring a 5 a.m. backup at 10 a.m. loses five hours of writes. Backups are not a substitute for high availability.
With Raft, bao operator raft snapshot save writes an atomic snapshot. There is no built in scheduler; the project maintains the openbao-snapshot-agent repository with systemd units for VMs and a CronJob for Kubernetes. A snapshot is encrypted by the barrier, so it is safe to copy to your normal backup target (our homelab backups guide covers 3-2-1 and restic), but it is useless without the unseal shares or auto unseal key that were current when it was taken.
NIST SP 800-57 makes the same point about key material in general: keys needed for as long as data is protected should be kept in both normal operational storage and backup storage, in separate locations. Your unseal shares are that key material. Store them on a different medium and in a different place from the snapshots.
The restore test is the step most labs skip. The procedure later in this post restores into a scratch instance on a laptop or VM, unseals it with the original shares, and reads a known canary secret. If that works, you have a backup. If it fails, you have learned that before you needed it.
How should secrets reach applications?
OpenBao Agent templates
The OpenBao Agent runs next to an application, logs in with auto auth (for example AppRole), keeps its token renewed, and renders templates to files. A template stanza names a source template, a destination file, file perms, and an optional command to run after rendering, such as reloading a service. The agent's renewal behaviour is documented precisely:
- Renewable secrets are renewed after two thirds of the lease has elapsed.
- Non renewable, non leased secrets such as KV version 2 are fetched again every five minutes by default (
static_secret_render_interval). - Non renewable leased secrets such as database credentials are fetched again when 85 percent of the TTL has passed.
That last rule is what makes a one hour database credential practical: the agent writes a new file at about 51 minutes, runs your reload command, and the old login expires a few minutes later.
Files rather than environment variables
The OWASP cheat sheet lists three ways to deliver secrets to containers: mounted files, in memory injection through a sidecar, and environment variables. It discourages environment variables unless the others are impossible, noting that "Environment variables are generally accessible to all processes", and they also turn up in crash dumps, debug pages and docker inspect. Render to a file on a tmpfs volume with mode 0400, owned by the service user.
Never in images, never in shell history
The cheat sheet also says mounted secret files should be added by the orchestrator and never baked into an image, and that secrets must never be hardcoded with ENV or ARG. Build arguments are recorded in image history. On the command line, avoid bao kv put ... password=hunter2: read values from a file with @file, from standard input with -, or let the CLI prompt. If you must type one, a leading space keeps it out of history only when your shell is configured for that (HISTCONTROL=ignorespace in bash), so check rather than assume.
Comparison: SOPS with age, a password manager, or a secrets server
| SOPS with age in Git | Self-hosted password manager | Secrets server (OpenBao) | |
|---|---|---|---|
| Built for | Configuration files that travel with code | People storing and sharing logins | Machines and people requesting credentials over an API |
| Audit of reads | None | Usually per user access history | Every request and response, hashed values, via audit devices |
| Rotation | Manual edit, commit, redeploy | Manual, with password generator help | KV versions plus agent re-render; dynamic and static database roles; PKI with short TTLs |
| Short lived credentials | No | No | Yes, leases with automatic revocation |
| Machine identity | Whoever has the private key | Not designed for machines | AppRole, Kubernetes, certificates, OIDC/JWT |
| Complexity | Low: two binaries and a key | Low to medium: one web service | Medium to high: a service, TLS, seal, audit, policies, backups |
| Works offline | Yes | Mostly, with cached vaults | No for new credentials; agent caches help |
| Failure mode | Lose the age key and you lose the secrets; leak it and every file is readable | Lose the master password; a server breach exposes encrypted vaults | Lose shares or auto unseal key and the server and its backups are locked; down server stops new credentials |
Most labs end up with two of these. A password manager for humans (see self-hosted password managers explained), and either SOPS or a secrets server for machines. A sensible hybrid keeps bootstrap secrets, such as the OpenBao TLS key or an AppRole SecretID for the configuration management host, in SOPS, and everything else in OpenBao.
Step by step: deploy OpenBao on a homelab VM
The walkthrough targets one Debian or Ubuntu VM (for example on Proxmox; see our Proxmox VE guide), named bao.home.arpa, with Raft storage, Shamir seal, two audit devices, a KV engine, a PostgreSQL database engine and an AppRole consumer. Adjust names and paths to your lab. Commands are taken from OpenBao's documentation for 2.7.
Step 1: install and verify the binary
Download the package for your platform from the OpenBao downloads page or the GitHub release, and verify it before installing. Releases publish checksums.txt with a GPG signature and a Sigstore bundle.
VERSION=2.7.1
# Download the .deb for your architecture plus the checksum files from the
# v${VERSION} GitHub release, then:
sha256sum --ignore-missing -c checksums.txt
sudo apt install ./openbao_*_linux_amd64.deb
bao version
If you prefer containers, the official images are ghcr.io/openbao/openbao, quay.io/openbao/openbao and docker.io/openbao/openbao, and they read configuration from /openbao/config/openbao.hcl. Pin a version tag rather than latest.
Step 2: issue TLS and write the server config
Issue a server certificate for bao.home.arpa from your home CA, then write /etc/openbao/openbao.hcl (the path the DEB and RPM packages use):
api_addr = "https://bao.home.arpa:8200"
cluster_addr = "https://bao.home.arpa:8201"
storage "raft" {
path = "/opt/openbao/data"
node_id = "bao1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/openbao/tls/bao.crt"
tls_key_file = "/etc/openbao/tls/bao.key"
tls_min_version = "tls13"
}
# Declarative audit devices; API creation is disabled by default since 2.3.2
audit "file" "local" {
description = "Local audit log, shipped to the log server"
options {
file_path = "/var/log/openbao/audit.log"
}
}
audit "syslog" "remote" {
description = "Second copy through syslog"
options {
tag = "openbao"
facility = "AUTH"
}
}
Lock down the files and start the service:
sudo install -d -o openbao -g openbao -m 0700 /opt/openbao/data /var/log/openbao
sudo chown -R openbao:openbao /etc/openbao/tls && sudo chmod 0600 /etc/openbao/tls/bao.key
sudo systemctl cat openbao | grep MemorySwapMax # expect MemorySwapMax=0
sudo systemctl enable --now openbao
export BAO_ADDR=https://bao.home.arpa:8200
bao status # Initialized false, Sealed true
Firewall port 8200 to the VLANs that need it and 8201 to cluster peers only. Our mini PC firewall guide shows the VLAN side.
Step 3: initialise and protect the shares
Decide the split before you run init. For a single operator, 3 shares with a threshold of 2 across three places you control works: one share in your password manager, one printed and sealed in a safe, one with a trusted person or in a safe deposit box. Encrypting each share to a different PGP public key at init time means the shares never appear in plain text on the server's terminal.
# Each .asc file holds one holder's PGP public key (exported, base64)
bao operator init \
-key-shares=3 \
-key-threshold=2 \
-pgp-keys="share1.asc,share2.asc,share3.asc" \
-root-token-pgp-key="admin.asc" \
-format=json > init.json
Distribute each encrypted share to its holder, confirm each holder can decrypt theirs, then securely delete init.json from the server. Do not keep all shares in one file, one password manager entry or one cloud drive folder: that recreates a single key with extra steps.
Step 4: unseal and log in with the root token
bao operator unseal # prompts; paste share 1 (decrypted by its holder)
bao operator unseal # share 2 reaches the threshold
bao status # Sealed false
bao login # paste the decrypted initial root token
Step 5: set up admin access, then revoke root
Create an admin policy and an OIDC or userpass login for yourself first, verify it works in a second terminal, and only then revoke the root token.
bao policy write lab-admin lab-admin.hcl
bao auth enable userpass
bao write auth/userpass/users/alex token_policies=lab-admin password=- # reads the password from stdin
# In a second terminal: bao login -method=userpass username=alex
bao token revoke -self # run in the root token session after the admin login works
Step 6: enable KV v2 and write a secret
bao secrets enable -path=kv -version=2 kv
bao kv put -mount=kv apps/nextcloud/smtp password=@smtp-password.txt
bao kv get -mount=kv -field=password apps/nextcloud/smtp
shred -u smtp-password.txt
KV v2 keeps 10 versions per key by default (max_versions of 0 means 10), so an accidental overwrite can be rolled back.
Step 7: enable the PostgreSQL database engine
Create a dedicated PostgreSQL role for OpenBao with permission to create roles, then:
bao secrets enable database
bao write database/config/lab-postgres \
plugin_name="postgresql-database-plugin" \
allowed_roles="nextcloud" \
connection_url="postgresql://{{username}}:{{password}}@db.home.arpa:5432/postgres?sslmode=verify-full" \
username="openbao" \
password=@openbao-db-password.txt \
password_authentication="scram-sha-256"
bao write database/roles/nextcloud \
db_name="lab-postgres" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT nextcloud_app TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
bao read database/creds/nextcloud # returns a new username, password and lease
Granting a pre-built group role such as nextcloud_app keeps table ownership stable while individual logins come and go. Once this works and you have a separate break glass login to PostgreSQL, rotate the connection's root credential so only OpenBao knows it:
bao write -f database/rotate-root/lab-postgres
Step 8: write the consumer policy and AppRole
bao policy write nextcloud-read nextcloud-read.hcl
bao auth enable approle
bao write auth/approle/role/nextcloud \
token_policies="nextcloud-read" \
token_ttl=1h token_max_ttl=4h \
secret_id_ttl=24h secret_id_num_uses=1 \
secret_id_bound_cidrs="10.20.30.15/32"
bao read -field=role_id auth/approle/role/nextcloud/role-id > role_id
bao write -f -field=secret_id auth/approle/role/nextcloud/secret-id > secret_id
Copy both files to the Nextcloud host (for example with your configuration management tool), owned by the agent's user with mode 0400. The SecretID is single use and expires in a day, so a copy left in a provisioning log is worthless shortly after.
Step 9: run the agent and render files
# /etc/openbao-agent/agent.hcl on the Nextcloud host
vault {
address = "https://bao.home.arpa:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/openbao-agent/role_id"
secret_id_file_path = "/etc/openbao-agent/secret_id"
}
}
}
template {
source = "/etc/openbao-agent/db.env.ctmpl"
destination = "/run/nextcloud/db.env"
perms = "0400"
command = "systemctl reload nextcloud"
error_on_missing_key = true
}
The template reads the dynamic credential and the KV secret, for example {{ with secret "database/creds/nextcloud" }}DB_USER={{ .Data.username }}{{ end }}. /run is a tmpfs on systemd hosts, so the rendered file never touches disk. Note that the agent configuration block is still named vault in OpenBao's agent docs; that is the configuration key, not a dependency.
Step 10: rotate a secret
For a KV secret, such as an SMTP relay password:
- Create the new password at the provider, keeping the old one valid.
- Write it as a new version:
bao kv put -mount=kv apps/nextcloud/smtp password=@new.txt. - Wait for the agent to re-render (up to five minutes by default) and confirm the service reloaded and sends mail.
- Revoke the old password at the provider.
- If something breaks,
bao kv rollback -mount=kv -version=<n> apps/nextcloud/smtprestores the previous version.
For dynamic database credentials there is nothing to rotate by hand; check that leases are being renewed and replaced with bao list sys/leases/lookup/database/creds/nextcloud from an admin session.
For the server's own keys, schedule two operations:
bao operator rotate # new data encryption key in the keyring; online
bao operator rotate-keys -init -key-shares=3 -key-threshold=2 \
-pgp-keys="share1.asc,share2.asc,share3.asc" -verify
# then each holder submits a current share with: bao operator rotate-keys
The -verify flag makes the new share holders prove they can use their new shares before the old ones stop working. That protects you from rotating into a set of shares nobody can decrypt.
Step 11: take a snapshot and test the restore
# On the server, from an admin session
bao operator raft snapshot save /var/backups/openbao/bao-$(date +%F).snap
sha256sum /var/backups/openbao/bao-*.snap
Add that to a systemd timer or use the project's snapshot agent, then let your normal backup tool copy the files off the host.
To prove the snapshot works, restore it into a scratch instance on a laptop or spare VM. Warning: a restore replaces all data on the target. Before running it, check that BAO_ADDR points at the scratch instance and not at production.
export BAO_ADDR=https://127.0.0.1:8200
bao status | grep -E "Initialized|Sealed" # confirm this is the scratch server
# Initialise the scratch server with throwaway keys, unseal it, log in with its root token,
# then force restore because the snapshot was sealed with different keys:
curl --cacert scratch-ca.pem \
--header "X-Vault-Token: $(cat scratch-root-token)" \
--request POST \
--data-binary @bao-2026-10-24.snap \
"$BAO_ADDR/v1/sys/storage/raft/snapshot-force"
# The scratch server now holds production data and needs production shares:
bao operator unseal # production share 1
bao operator unseal # production share 2
bao login -method=userpass username=alex
bao kv get -mount=kv -field=value restore-test/canary
The force endpoint is documented as bypassing the checks that the snapshot's keys match the running server, which is what a cross instance restore needs. Write a canary secret with a known value before each snapshot, compare it after the restore, then destroy the scratch instance and its data directory. Doing this once a quarter, or after every rotation of the shares, is what turns a backup into a recovery plan.
Common mistakes
- Storing the unseal shares next to the server. A text file of shares in the VM's home directory, or all shares in one notes app, means anyone who gets the disk image or that account gets everything. NIST's split knowledge model only works if shares are in separate places.
- Keeping the root token for daily use. The initial root token never expires. Using it every day means one leaked shell history or one compromised workstation owns the whole server. Revoke it and use
generate-rootwith a quorum when you truly need root. - Never testing a restore. A snapshot that has never been restored may be corrupt, may need shares nobody still has, or may depend on an auto unseal key that was deleted. The OpenBao docs warn that losing the seal mechanism makes even backups unrecoverable.
- Static key auto unseal with the key on the same disk. Convenient at reboot, but it collapses the three layer key hierarchy into a single file.
- No audit device, or only one. A fresh server logs nothing, and with a single device that fills its disk, OpenBao stops answering. Declare two.
- Enabling
log_raw. It writes secret values into the audit log in plain text. - Policies on
kv/*. A wildcard over a whole mount gives every consumer every secret. Grant per application paths underdata/. - Long lived AppRole SecretIDs. A SecretID with no TTL and unlimited uses is a static password with a new name. Limit uses, TTL and source addresses.
- Running the dev server as the real server.
bao server -devstarts unsealed with an in memory backend and a known root token. It is for learning only. - Secrets in
docker buildarguments or environment variables. Build arguments end up in image history and environment variables leak through inspection and crash output. Render files to tmpfs instead. - Forgetting key cryptoperiods. NIST SP 800-57 Table 1 suggests an originator usage period of under two years for symmetric data encryption and key wrapping keys. OpenBao rotates the data encryption key automatically before roughly 2 to the power 32 encryptions, following NIST SP 800-38D, but scheduled rotation of shares and the keyring is a calendar entry you have to own.
- Not upgrading OpenBao. The 2.7.1 and 2.6.4 releases on 1 October 2026 carried security fixes. A secrets server is the last thing to leave on an old version.
FAQ
Is OpenBao free to use in a homelab?
Yes. OpenBao is licensed under the Mozilla Public License 2.0, and the project states its intent to stay under an OSI approved open source licence with community governance. There is no paid edition of OpenBao itself.
What is the difference between unseal keys and recovery keys?
Unseal keys are shares of the key that decrypts OpenBao's root key, and they are used with the default Shamir seal. Recovery keys are produced instead when you use auto unseal. They authorise quorum operations such as generating a root token or rotating keys, but they cannot decrypt the root key, so they cannot unseal the server if the auto unseal mechanism is unavailable.
How many unseal key shares should a homelab use?
The default is 5 shares with a threshold of 3. A single operator often uses 3 shares with a threshold of 2, spread across a password manager, a printed copy in a safe and a trusted person or second location. The goal is that losing any one location neither exposes the key nor locks you out.
Do I have to unseal OpenBao after every reboot?
With the default Shamir seal, yes: a node stays unsealed only until it restarts. Auto unseal (Transit, static key, PKCS#11 or a cloud KMS plugin) removes that step at the cost of depending on the seal mechanism being available, and protected, at every start.
Can I back up OpenBao by copying the data directory?
Only while the service is stopped. The docs recommend offline backups for any backend without atomic snapshots. With Integrated Storage (Raft), bao operator raft snapshot save takes a consistent snapshot of a running server. Either way the backup is encrypted, so you also need the unseal shares or auto unseal key that were current when it was taken.
What are dynamic secrets in OpenBao?
Credentials that OpenBao creates on request and deletes when their lease ends. With the database engine, each read of database/creds/<role> creates a new database login with its own password and a TTL such as one hour, and revoking the lease or the token that requested it removes the login.
Should applications read secrets from environment variables or files?
Files, where you can. The OWASP Secrets Management Cheat Sheet discourages environment variables because they are generally readable by all processes and leak into logs and diagnostics. The OpenBao Agent can render secrets to a file with restrictive permissions on a tmpfs path and reload the service when they change.
Is OpenBao overkill for a small homelab?
It can be. If you run a few services with one operator and never need short lived credentials, SOPS with age in Git plus a password manager is simpler and fails more gracefully. OpenBao earns its keep when you want read audit, dynamic database credentials, an intermediate CA, or per machine identity.
How often should I rotate OpenBao's keys?
NIST SP 800-57 suggests under two years of use for symmetric data encryption and key wrapping keys. Rotating the keyring with bao operator rotate and the unseal shares with bao operator rotate-keys once a year, and immediately whenever a share holder leaves or a share may be exposed, keeps you well inside that.
What is the difference between operator rekey and operator rotate-keys?
They do the same job: generate a new set of unseal or recovery key shares, optionally with a new number of shares or threshold. rotate-keys was added in OpenBao 2.4.0 and rekey is now deprecated, so new scripts should use rotate-keys.
Where Safeguard fits
Safeguard does not store, issue or broker secrets, and it is not a replacement for OpenBao, SOPS or a password manager. Its fit here is indirect and sits on the software side: the OpenBao server binary or container image, the agent on every host, and the applications that consume credentials are all built from open source components that need patching. Safeguard can scan repositories, container images and SBOM files (safeguard scan --dir ., --image <name>, --sbom <file>), match the components it finds against vulnerability databases, show which of your projects contain a given component and version, and open fix pull requests when you switch autonomous remediation on. A homelab that pins ghcr.io/openbao/openbao to a version tag can scan that image the same way it scans its own services, so an outdated secrets server shows up next to everything else that needs an upgrade.
A secrets server sits in the middle of everything else you run, so the software around it matters as much as its keys: the server and agent binaries, their container base images and the applications that read the credentials all ship open source dependencies. Keeping those current is ordinary supply chain hygiene, and the steps below apply to the parts you build and deploy yourself; fork and patch is early access.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. The assistant reads a finding and its advisory, asks before each command, upgrades a dependency, runs the tests and pushes nothing. The same flow fits a homelab repository that builds a service image on top of a pinned OpenBao or application base image: find the outdated component, approve the upgrade, test, and push it yourself.
Click through it at your own pace, or open it full screen.
The second demo sets up the Safeguard runner on your own machine, approves it with a device code, then scans from the terminal. A secrets server usually lives on an inner VLAN, and running the scan from a machine you control means you do not have to open that network up to inspect the images and repositories that run there.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. It is the same least privilege idea this post applies to OpenBao policies: an assistant gets the specific tools you grant, not everything by default.
Click through it at your own pace, or open it full screen.
Download the apps at safeguard.sh/download and read the CLI docs at docs.safeguard.sh/docs/cli.
Quick check
Your OpenBao server uses auto unseal with a cloud KMS key, and someone deletes that key. Can you restore last night's Raft snapshot to get your secrets back?
No. The snapshot is encrypted by the barrier, and the root key that unlocks it was wrapped by the deleted KMS key. Recovery keys only authorise operations; they cannot decrypt the root key. OpenBao's seal docs state that if the seal mechanism or its keys are permanently deleted, the cluster cannot be recovered, even from backups.
A database credential issued by OpenBao leaks in a log. You restart the application. Is the leaked credential still valid?
Yes, until its lease expires or is revoked. Restarting the consumer does not revoke anything. Run bao lease revoke on that lease, or bao lease revoke -prefix database/creds/<role> to drop every login the role has issued, and the database logins are removed immediately.
Sources
- OpenBao documentation home
- OpenBao: What is OpenBao?
- OpenBao: Seal/Unseal concepts
- OpenBao: static key seal
- OpenBao: Transit seal
- OpenBao: operator init
- OpenBao: operator rotate-keys
- OpenBao: operator rotate
- OpenBao: key rotation internals
- OpenBao: storage and backups
- OpenBao: operator raft snapshot
- OpenBao: Integrated Storage (Raft) API, including snapshot-force
- OpenBao: PebbleDB storage backend
- OpenBao: audit devices
- OpenBao: declarative audit configuration
- OpenBao: tokens and root tokens
- OpenBao: policies
- OpenBao: leases, renewal and revocation
- OpenBao: AppRole auth method
- OpenBao: JWT/OIDC auth method
- OpenBao: Kubernetes auth method
- OpenBao: database secrets engine
- OpenBao: PostgreSQL database plugin
- OpenBao: PKI intermediate CA quick start
- OpenBao: PKI considerations, ACME and EAB
- OpenBao: KV version 2
- OpenBao Agent templates
- OpenBao: TCP listener configuration
- OpenBao: installation, swap and signature verification
- OpenBao: operator quick start
- OpenBao CHANGELOG
- OpenBao repository and licence
- OpenBao blog: Announcing OpenBao v2.0.0 (25 July 2024)
- OpenBao blog: OpenBao Joins the OpenSSF (17 June 2025)
- OpenBao blog: Announcing OpenBao v2.7 (5 October 2026)
- OpenSSF: OpenBao project page
- NIST SP 800-57 Part 1 Rev. 5: Recommendation for Key Management (May 2020)
- NIST SP 800-57 Part 1 Rev. 5 full text (PDF)
- OWASP Secrets Management Cheat Sheet
- SOPS repository
- age repository
Related reading: age and SOPS for Git native secrets, running your own certificate authority at home, self-hosted password managers explained and building a tiny data center at home.
Last reviewed: 24 October 2026. Written against OpenBao 2.7.1; OpenBao ships frequent releases, so check the project's changelog and documentation before relying on a specific default or command.