Self-Hosted OpenBao (HashiCorp Vault Fork)
Credentials and API keys can be quite a pain, you can store them in a password manager, but then your servers and services won’t have access to it, keeping them in local files or ENV Vars is also a bit problematic since they can get out of sync. To solve this, you can for example use something like hashicorp Vault, but with the change of hashicorp vault to a BSL License you might also wonder if you are setting yourself up for some future problems of further license changes. Here is where OpenBao comes to the rescue.
OpenBao is an open source, community-driven fork of Vault managed by the Linux Foundation.
So far the setup and usage is the same as hashicorp vault and it’s API compatible. So let’s dive in and have some fun.
Preparations
I will show the setup based on three separate servers all of them running docker and communicating internally with self signed certificates.
The base installation will be done in /opt/docker/vault, you can change this path as you see fit or use docker volumes if that’s your preference.
Preparing the servers
The assumption is that you already have docker and docker compose. In addition to that, some internal IP going over all three servers, or the public IP works as well.
| Server | Internal IP |
|---|---|
| vault01 | 10.0.0.4 |
| vault02 | 10.0.0.2 |
| vault03 | 10.0.0.3 |
Preparing the directories
We will start on the any of the three servers and create the directories.
mkdir -p /opt/docker/vault/{certs,config,data}
chown 100:1000 /opt/docker/vault/{certs,config,data}
Here we will create the base directory /opt/docker/vault/ and three subdirectories certs,config,data then change owner and group to the OpenBao used user id and group id.
Preparing the certificates
Still on the same server you picked at the start, create all files to prepare the certificates.
Main cert file
cd /opt/docker/vault/certs/
cat > extfile.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[ req_distinguished_name ]
C = AT
ST = Vienna
L = Vienna
O = cloudvox.at
CN = RootCA
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF
vault01 cert file
Make sure the IP.1 matches to the IP’s from your infrastructure.
cat > extfile01.cnf <<EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = AT
ST = Vienna
L = Vienna
O = cloudvox.at
CN = vault01
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.0.0.4
IP.2 = 127.0.0.1
DNS.1 = vault01
DNS.2 = vault
EOF
vault02 cert file
cat > extfile02.cnf <<EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = AT
ST = Vienna
L = Vienna
O = cloudvox.at
CN = vault02
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.0.0.2
IP.2 = 127.0.0.1
DNS.1 = vault02
DNS.2 = vault
EOF
vault03 cert file
cat > extfile03.cnf <<EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = AT
ST = Vienna
L = Vienna
O = cloudvox.at
CN = vault03
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.0.0.3
IP.2 = 127.0.0.1
DNS.1 = vault03
DNS.2 = vault
EOF
Create the certificates
# CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config extfile.cnf
# Host 01
openssl genrsa -out vault01.key 4096
openssl req -new -key rsa:4096 -key vault01.key -out vault01.csr -config extfile01.cnf
openssl x509 -req -days 3650 -in vault01.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault01.crt -extensions req_ext -extfile extfile01.cnf
# Host 02
openssl genrsa -out vault02.key 4096
openssl req -new -key rsa:4096 -key vault02.key -out vault02.csr -config extfile02.cnf
openssl x509 -req -days 3650 -in vault02.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault02.crt -extensions req_ext -extfile extfile02.cnf
# Host 03
openssl genrsa -out vault03.key 4096
openssl req -new -key rsa:4096 -key vault03.key -out vault03.csr -config extfile03.cnf
openssl x509 -req -days 3650 -in vault03.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault03.crt -extensions req_ext -extfile extfile03.cnf
fixing permissions
Fix permissions of the newly created certificates files
chown 100:1000 /opt/docker/vault/certs -R
Prepare docker compose file
Create a docker compose file, still on the chosen first server.
cd /opt/docker/vault/
cat > compose.yml << EOF
services:
openbao:
image: openbao/openbao:2.4.4
container_name: openbao
restart: unless-stopped
extra_hosts:
- "vault01:10.0.0.4"
- "vault02:10.0.0.2"
- "vault03:10.0.0.3"
cap_add:
- IPC_LOCK
ports:
- "8200:8200"
- "8201:8201"
environment:
BAO_LOG_LEVEL: "info"
volumes:
- /opt/docker/vault/data:/openbao/data
- /opt/docker/vault/config:/openbao/config
- /opt/docker/vault/certs:/certs:ro
command: ["server", "-config=/openbao/config/openbao.hcl"]
EOF
Make sure to insert your IP address under the extra_hosts section.
What this does is create a DNS entry on the docker network that will resolve vault01 to 10.0.0.4, vault02 to 10.0.0.2 and vault03 to 10.0.0.3, this way inside the containers the dns for vault01,02 and 03 will work.
Now create a tarball and copy this file to all three servers and untar it.
tar czvf /tmp/vault.tgz vault/
Configuring OpenBao
Now to create the OpenBao configuration file
vault01
Create this file on the first server only. You have to be careful to use the correct node_id and tls_cert_* according to the correct server you are on, as well as make sure it connects to the other two servers.
cd /opt/docker/vault/config/
cat > openbao.hcl << EOF
api_addr = "http://0.0.0.0:8200"
cluster_addr = "https://10.0.0.4:8201"
ui = true
disable_mlock = true
storage "raft" {
path = "/openbao/data"
node_id = "vault01"
retry_join {
leader_api_addr = "https://vault02:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault02.crt"
leader_client_key_file = "/certs/vault02.key"
}
retry_join {
leader_api_addr = "https://vault03:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault03.crt"
leader_client_key_file = "/certs/vault03.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0
tls_cert_file = "/certs/vault01.crt"
tls_key_file = "/certs/vault01.key"
}
EOF
vault02
Create this file on the second server only.
cd /opt/docker/vault/config/
cat > openbao.hcl << EOF
api_addr = "http://0.0.0.0:8200"
cluster_addr = "https://10.0.0.2:8201"
ui = true
disable_mlock = true
storage "raft" {
path = "/openbao/data"
node_id = "vault02"
retry_join {
leader_api_addr = "https://vault03:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault03.crt"
leader_client_key_file = "/certs/vault03.key"
}
retry_join {
leader_api_addr = "https://vault01:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault01.crt"
leader_client_key_file = "/certs/vault01.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0
tls_cert_file = "/certs/vault02.crt"
tls_key_file = "/certs/vault02.key"
}
EOF
vault03
Create this file on the third server only.
cd /opt/docker/vault/config/
cat > openbao.hcl << EOF
api_addr = "http://0.0.0.0:8200"
cluster_addr = "https://10.0.0.3:8201"
ui = true
disable_mlock = true
storage "raft" {
path = "/openbao/data"
node_id = "vault03"
retry_join {
leader_api_addr = "https://vault02:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault02.crt"
leader_client_key_file = "/certs/vault02.key"
}
retry_join {
leader_api_addr = "https://vault01:8200"
leader_ca_cert_file = "/certs/ca.crt"
leader_client_cert_file = "/certs/vault01.crt"
leader_client_key_file = "/certs/vault01.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0
tls_cert_file = "/certs/vault03.crt"
tls_key_file = "/certs/vault03.key"
}
EOF
Start the container
With the config out of the way, you can now start the container on the first server.
docker compose up -d
you will see errors since OpenBao is only running on one node so far.
openbao | 2026-01-02T14:42:36.262Z [INFO] core: attempting to join possible raft leader node: leader_addr=https://vault03:8200
openbao | 2026-01-02T14:42:36.262Z [INFO] core: attempting to join possible raft leader node: leader_addr=https://vault02:8200
Start OpenBao on the remaining servers.
This will now still show errors about the vault being sealed, ignore this for now.
openbao | 2026-01-02T14:44:11.934Z [ERROR] core: failed to get raft challenge: leader_addr=https://vault02:8200
openbao | error=
openbao | | error during raft bootstrap init call: Error making API request.
openbao | |
openbao | | URL: PUT https://vault02:8200/v1/sys/storage/raft/bootstrap/challenge
openbao | | Code: 503. Errors:
openbao | |
openbao | | * Vault is sealed
Init the vault
First, let’s check the status:
docker exec -e BAO_CACERT=/certs/ca.crt -ti openbao ash
bao status
This should show you something like this:
/ # bao status
Key Value
--- -----
Seal Type shamir
Initialized false
Sealed true
Total Shares 0
Threshold 0
Unseal Progress 0/0
Unseal Nonce n/a
Version 2.4.4
Build Date 2025-11-24T19:54:48Z
Storage Type raft
HA Enabled true
We see two issues here: Initialized = false and Sealed = true. Let’s start with the first issue
bao operator init
This will give you an initial root token and five unseal keys. Save this data somewhere safe and NEVER share it!. Since this is just a demo system that I will wipe once I am done with this post, I am sharing the output here, but never share this data anywhere.
/ # bao operator init
Unseal Key 1: Oolxx5Q1DKdB0zEVPCJZhq1EoPnGwhy6B/azsH75V+Ya
Unseal Key 2: /s6xL51RGwd+8TJTLX3wAMfbsQYaWOsuL8NGMdtfUfkU
Unseal Key 3: trC9EUZmEVNbv94Ux0Muk6EQPiqwemoN9di/LPdXpwFc
Unseal Key 4: jJ55vOcdlVtmMdpEPsyOVX5zWEc5EoRCRvae8etlsyI2
Unseal Key 5: JVJV1iegHg3hL4wHsxfPGmc0MIUy4rPcZhJ+rt7tMC2s
Initial Root Token: s.M5H4jvyvIVsqWUm1189rOB6m
Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.
Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!
It is possible to generate new unseal keys, provided you have a quorum
of existing unseal keys shares. See "bao operator rotate-keys" for more
information.
Unsealing
on EVERY one of the three serves, unseal the vault. You have to execute the unseal command three times on every node in order to unseal the vault.
/ # bao operator unseal
Unseal Key (will be hidden):
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 5
Threshold 3
Unseal Progress 1/3
Unseal Nonce 23b4859c-094b-fcfc-52e0-56991b223ffd
Version 2.4.4
Build Date 2025-11-24T19:54:48Z
Storage Type raft
HA Enabled true
once you have done this, three times, you will see that the Sealed will show up as false, indicating that the vault is now unsealed.
/ # bao status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 5
Threshold 3
Version 2.4.4
Build Date 2025-11-24T19:54:48Z
Storage Type raft
Cluster Name vault-cluster-a073a5ff
Cluster ID 8cc6ba91-aa6d-701c-4438-6a4df87673da
HA Enabled true
HA Cluster https://10.0.0.2:8201
HA Mode active
Active Since 2026-01-02T14:58:39.912484393Z
Raft Committed Index 31
Raft Applied Index 31
The errors should now have stopped and your OpenBao Vault is up and running.
Installing client on macOS
We can continue from this point by working on the server inside the docker container, or install the bao client on MacOS. I prefer the second option so let’s go ahead and install the client on Mac.
brew install openbao
If you now try to run bao status, it will of course not work, you haven’t told bao where it should actually connect to.
bao status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": dial tcp 127.0.0.1:8200: connect: connection refused
To tell bao where to connect, you need to define BAO_ADDR with the address, however, since this setup is using a self sigend certificate, you won’t get so far, yet.
export BAO_ADDR="https://10.0.0.2:8200"
bao status
Error checking seal status: Get "https://10.0.0.2:8200/v1/sys/seal-status": tls: failed to verify certificate: x509: “vault02” certificate is not standards compliant
To fix this issue, copy the ca.crt to your local machine and start by providing the BAO_CACERT env var.
BAO_CACERT=./ca.crt bao status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 5
Threshold 3
Version 2.4.4
Build Date 2025-11-24T19:54:48Z
Storage Type raft
Cluster Name vault-cluster-a073a5ff
Cluster ID 8cc6ba91-aa6d-701c-4438-6a4df87673da
HA Enabled true
HA Cluster n/a
HA Mode standby
Active Node Address <none>
Raft Committed Index 37
Raft Applied Index 37
Logging in to the OpenBao vault
Now that everything is up and running, you can log in with the root token.
BAO_CACERT=./ca.crt bao login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below is
already stored in the token helper. You do NOT need to run "bao login" again.
Future OpenBao requests will automatically use this token.
Key Value
--- -----
token s.m7phHuio6Vvg1rPRzYV0TqBa
token_accessor bxvR4r2ZPTTFK0kuiVfIDzX2
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Checking peer status
After logging in, you are able to check the peer status and see all the nodes in the cluster.
BAO_CACERT=./ca.crt bao operator raft list-peers
Node Address State Voter
---- ------- ----- -----
vault02 10.0.0.2:8201 leader true
vault01 10.0.0.4:8201 follower false
vault03 10.0.0.3:8201 follower false
Failover
If a node is shut down, or crashes, one of the two other nodes will take over being the master node, in this example I have shut down vault02, vault01 took over being the leader.
/ # bao operator raft list-peers
Node Address State Voter
---- ------- ----- -----
vault02 10.0.0.2:8201 follower true
vault01 10.0.0.4:8201 leader true
vault03 10.0.0.3:8201 follower true
After starting vault02 back, it will not automatically become leader again. Also keep in mind that that once restarted it will start up sealed and you need to unseal the vault on that particular node again.
Wrapping it up
While this does setup a replicated vault over three nodes, it’s still a stretch before this can be used in production. There’s no monitoring to alert in case of a restart, there is no auto unseal, no health checks. Also while this is replicated, it still requires to use a custom CA to prevent ssl errors and you would have to change the IP you are connecting to in order to be able to communicate with one of the other vaults. But it’s a good starting point to get OpenBao up and running. In the next part we will dead with the HighAvailability setup, monitoring and finally start adding some secrets to the vault.
Credits
Thanks to Dr. Maximilian Kalus for sharing his setup of HashiCorp Vault which made my setup and this post possible. Photo by Jason Pofahl on Unsplash
