A production deployment guide for self-hosted secrets management using OpenBao, the Linux Foundation fork of HashiCorp Vault.
Overview
OpenBao is an open-source, community-driven fork of HashiCorp Vault, governed by the Linux Foundation and the OpenSSF. It provides identity-based secrets management, dynamic secrets, and encryption as a service under the MPL 2.0 license, with no per-secret or per-request pricing. When you self-host it on a RamNode VPS you get full control over your secrets infrastructure at a flat monthly cost.
This guide covers a single-node production install using the native binary, systemd, integrated Raft storage, and TLS. Raft removes the need for an external database, which keeps the deployment self-contained and easy to back up.
The current stable series is OpenBao 2.x. The bao CLI is the primary binary and is a drop-in analogue of the vault command.
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS (Debian 12 works with the same steps). A 2 GB RAM plan is comfortable for most single-node use cases; 1 GB is workable for light workloads.
- A domain or subdomain pointing at your VPS public IP (for example
bao.example.com) if you want browser-trusted TLS. - Root or sudo access.
- Basic familiarity with Linux service administration.
1. Initial server hardening
Create a non-root administrative user and apply baseline hardening before installing anything.
# As root
adduser deploy
usermod -aG sudo deploy
# Copy your SSH key to the new user
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployHarden SSH by editing /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication noThen reload:
systemctl reload sshConfigure the firewall. OpenBao listens on 8200 (API/UI) and 8201 (cluster). For a single node you only need 8200 exposed, and ideally only behind TLS.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 8200/tcp
sudo ufw enable2. Install the OpenBao binary
Fetch the latest release binary directly from GitHub. This snippet resolves the newest version automatically so it stays current.
sudo apt update && sudo apt install -y curl jq unzip
VERSION=$(curl -s https://api.github.com/repos/openbao/openbao/releases/latest \
| jq -r '.tag_name' | cut -d'v' -f2-)
ARCH=$(dpkg --print-architecture) # amd64 or arm64
curl -Lo bao.tar.gz \
"https://github.com/openbao/openbao/releases/download/v${VERSION}/bao_${VERSION}_linux_${ARCH}.tar.gz"
tar -xzf bao.tar.gz bao
sudo install -o root -g root -m 0755 bao /usr/local/bin/bao
rm -f bao bao.tar.gz
bao versionOpenBao also ships native packages for Debian, Ubuntu, RHEL, and Fedora on the downloads page if you prefer apt-managed upgrades. The binary approach shown here keeps the install self-contained and predictable.
3. Create the service account and directories
sudo useradd --system --home /etc/openbao --shell /bin/false openbao
sudo mkdir -p /etc/openbao /opt/openbao/data /opt/openbao/tls
sudo chown -R openbao:openbao /opt/openbao
sudo chmod 700 /opt/openbao/data4. Write the server configuration
Create /etc/openbao/openbao.hcl:
cluster_name = "ramnode-openbao"
ui = true
storage "raft" {
path = "/opt/openbao/data"
node_id = "node1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/opt/openbao/tls/fullchain.pem"
tls_key_file = "/opt/openbao/tls/privkey.pem"
}
api_addr = "https://bao.example.com:8200"
cluster_addr = "https://127.0.0.1:8201"
default_lease_ttl = "168h"
max_lease_ttl = "720h"
disable_mlock = falseReplace bao.example.com with your domain. Notes on the choices:
disable_mlock = falsekeeps secret material out of swap. Because OpenBao locks memory, the systemd unit below grants theIPC_LOCKcapability. As a defense in depth measure you should also disable or encrypt swap on the host.- TLS is terminated by OpenBao itself rather than a proxy. For a secrets manager this is the more defensible design because plaintext secrets never traverse a local socket in the clear.
Disable or encrypt swap
# Confirm current swap
sudo swapon --show
# To disable swap entirely (simplest for a dedicated secrets host)
sudo swapoff -a
sudo sed -i '/\sswap\s/d' /etc/fstab5. Provision TLS certificates
Option A: Let's Encrypt with Caddy in front (simplest)
If you would rather not manage certificate renewals by hand, put Caddy in front and disable native TLS in the listener (set the listener to 127.0.0.1:8200 and tls_disable = true, then point api_addr at the HTTPS Caddy address). A minimal Caddyfile:
bao.example.com {
reverse_proxy 127.0.0.1:8200
}Caddy obtains and renews the certificate automatically. With this model, only 80 and 443 need to be open in UFW, and 8200 stays bound to localhost.
Option B: Native TLS with certbot (hardened)
Obtain a certificate and copy it into place for OpenBao to read directly:
sudo apt install -y certbot
sudo certbot certonly --standalone -d bao.example.com
sudo cp /etc/letsencrypt/live/bao.example.com/fullchain.pem /opt/openbao/tls/
sudo cp /etc/letsencrypt/live/bao.example.com/privkey.pem /opt/openbao/tls/
sudo chown openbao:openbao /opt/openbao/tls/*.pem
sudo chmod 600 /opt/openbao/tls/*.pemAdd a renewal hook so OpenBao picks up rotated certs. Create /etc/letsencrypt/renewal-hooks/deploy/openbao.sh:
#!/bin/bash
cp /etc/letsencrypt/live/bao.example.com/fullchain.pem /opt/openbao/tls/
cp /etc/letsencrypt/live/bao.example.com/privkey.pem /opt/openbao/tls/
chown openbao:openbao /opt/openbao/tls/*.pem
chmod 600 /opt/openbao/tls/*.pem
systemctl reload openbaosudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/openbao.sh6. Create the systemd unit
Create /etc/systemd/system/openbao.service:
[Unit]
Description=OpenBao secrets management
Documentation=https://openbao.org/docs/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/openbao/openbao.hcl
[Service]
User=openbao
Group=openbao
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/local/bin/bao server -config=/etc/openbao/openbao.hcl
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now openbao
sudo systemctl status openbao7. Initialize and unseal
Point the CLI at your server and initialize. This produces unseal keys and the initial root token. Store these somewhere safe and offline; losing them means losing access to your data.
export BAO_ADDR="https://bao.example.com:8200"
bao operator init -key-shares=5 -key-threshold=3Unseal by supplying three of the five keys (run the command three times with different keys):
bao operator unsealLog in with the root token:
bao login <root-token>
bao statusA healthy node reports Sealed false and Initialized true.
Auto-unseal (recommended for production)
Manual unsealing on every restart is operationally painful. OpenBao supports auto-unseal via cloud KMS or a Transit seal from another OpenBao instance. If you run a second small VPS, a Transit auto-unseal removes the manual step while keeping the root of trust off the primary host. Configure a seal "transit" stanza pointing at the second instance once you are ready to scale beyond a single node.
8. First secret and the UI
Enable the KV v2 secrets engine and write a test secret:
bao secrets enable -version=2 kv
bao kv put kv/test username=admin password=s3cr3t
bao kv get kv/testThe web UI is available at https://bao.example.com:8200/ui (or your Caddy address). Log in with any valid token.
For remote administration without exposing the UI publicly, an SSH tunnel is a clean option:
ssh -L 8200:127.0.0.1:8200 deploy@your-ramnode-ip9. Backups
Raft storage supports consistent point-in-time snapshots. Automate them.
# Manual snapshot
bao operator raft snapshot save /opt/openbao/backups/bao-$(date +%F).snapA simple daily cron under root, writing to a directory the openbao user owns and shipping off-box:
0 3 * * * BAO_ADDR="https://bao.example.com:8200" BAO_TOKEN="<backup-token>" \
/usr/local/bin/bao operator raft snapshot save \
/opt/openbao/backups/bao-$(date +\%F).snapUse a dedicated token with a policy limited to snapshot operations rather than the root token. Push snapshots to off-site object storage; RamNode S3-compatible storage or any external bucket works well. Restore with bao operator raft snapshot restore.
10. Upgrades
Because the install is a single binary, upgrades are straightforward:
# Fetch and install the new binary (repeat the step 2 commands)
sudo systemctl restart openbao
# Then unseal again unless you have configured auto-unseal
bao operator unsealRead the release notes before upgrading. Some provider-specific built-in seals are being moved to external plugins in later releases, and there are periodic security fixes worth tracking on the OpenBao releases page.
RamNode platform notes
- OpenBao does not send email, so RamNode's prohibition on running mail services does not affect it. If you wire OpenBao events into an alerting pipeline, route notifications through an external API-based service rather than an on-box SMTP relay.
- Keep 8200 closed to the public internet until TLS is active. Never expose an OpenBao listener with
tls_disable = trueon a public interface.
Troubleshooting
- Server starts but stays sealed after a reboot: this is expected without auto-unseal. Run
bao operator unsealthree times, or configure a Transit or KMS seal. Error checking seal status ... connection refused: confirmBAO_ADDRuseshttpsand matches yourapi_addr, and that the service is running.- Permission denied on the data directory: confirm
/opt/openbao/datais owned byopenbaoand is mode 700. - mlock errors in the journal: confirm the systemd unit grants
CAP_IPC_LOCKand setsLimitMEMLOCK=infinity.
Check logs with:
journalctl -u openbao -f